﻿#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)

#include <stdio.h>

//程序的翻译环境
//程序的执行环境
//详解：C语言程序的编译 + 链接
//预定义符号介绍
//预处理指令 #define
//宏和函数的对比
//预处理操作符#和##的介绍
//命令定义
//预处理指令 #include
//预处理指令 #undef
//条件编译

/* 1. 程序的翻译环境和执行环境 */
//在ANSI C的任何一种实现中，存在两个不同的环境。
//第1种是翻译环境，在这个环境中源代码被转换为可执行的机器指令。
//第2种是执行环境，它用于实际执行代码。

/*2. 详解编译 + 链接*/
//2.1 翻译环境
//组成一个程序的每个源文件(.c文件)通过编译器转换成目标文件（object）。
//linux环境下用gcc编译器编译后生成的文件是以.o为后缀的目标文件
//每个目标文件由链接器（linker）捆绑在一起，形成一个单一而完整的可执行程序（.exe文件）。
//链接器同时也会引入标准C函数库中任何被该程序所用到的函数，而且它可以搜索程序员个人的程序库，将其需要的函数也链接到程序中。

//2.2 编译本身也分为几个阶段：
//1. 预处理 选项 gcc - E test.c - o test.i
//预处理完成之后就停下来，预处理之后产生的结果都放在test.i文件中。
//2. 编译：gcc test.i -S (gcc test.c -S)；把C语言代码转换成汇编代码
//编译完成之后就停下来，结果保存在test.s中。test.s都是汇编代码
//整个编译过程很复杂，主要有4步：语法分析，词法分析，语义分析，符号汇总。
//符号汇总会把写的代码当中所有的全局符号汇总起来，如g_val,ADD,main等
//3. 汇编 gcc test.s - c；把汇编代码转化成二进制指令并形成符号表
//汇编完成之后就停下来，结果保存在test.o(二进制文件)中。

// 链接有两部分：
// 合并段表：把相同格式的文件进行合并（由编译生成的.o文件在linux环境下的格式是elf格式，linux环境下的可执行程序也是elf格式）
// 符号表的合并和重定位
// 最后就可以跨文件找到函数

//2.3 运行环境
//程序执行的过程：
//1. 程序必须载入内存中。在有操作系统的环境中：一般这个由操作系统完成。在独立的环境中，程序的载入必须由手工安排，也可能是通过可执行代码置入只读内存来完成。
//2. 程序的执行便开始。接着便调用main函数。
//3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈（stack），存储函数的局部变量和返回地址。程序同时也可以使用静态（static）内存，存储于静态内存中的变量在程序的整个执行过程
//一直保留他们的值。
//4. 终止程序。正常终止main函数；也有可能是意外终止。

/* 3. 预处理详解 */

//3.1 预定义符号
//__FILE__      //进行编译的源文件
//__LINE__      //文件当前的行号
//__DATE__      //文件被编译的日期
//__TIME__      //文件被编译的时间
//__STDC__      //如果编译器遵循ANSI C，其值为1，否则未定义
//这些预定义符号都是语言内置的。
//int main()
//{
//	int i = 0;
//	for (i = 0; i < 10; i++)
//	{
//		printf("file:%s line=%d i=%d\n", __FILE__, __LINE__, i);
//	}
//
//	return 0;
//}

//3.2 #define
//3.2.1 #define 定义标识符
//#define MAX 1000
//#define reg register          //为 register这个关键字，创建一个简短的名字
//#define do_forever for(;;)    //用更形象的符号来替换一种实现
//#define CASE break;case       //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长，可以分成几行写，除了最后一行外，每行的后面都加一个反斜杠(续行符)。
//#define DEBUG_PRINT printf("file:%s\tline:%d\t \
//                          date:%s\ttime:%s\n" ,\
//                          __FILE__,__LINE__ ,  \
//                          __DATE__,__TIME__ )

//3.2.2 #define 定义宏
//#define 机制包括了一个规定，允许把参数替换到文本中，这种实现通常称为宏（macro）或定义宏（define macro）。
//下面是宏的申明方式：
//#define name( parament-list ) stuff
//其中的 parament - list 是一个由逗号隔开的符号表，它们可能出现在stuff中。
//注意：
//参数列表的左括号必须与name紧邻。
//如果两者之间有任何空白存在，参数列表就会被解释为stuff的一部分。

//#define SQUARE(x) x*x			//SQUARE(x)为宏名和参数；宏体x*x
//int main()
//{
//	int i = SQUARE(5);			//25
//	int r = SQUARE(5+1);		//只替换，不计算 int r=5+1*5+1=11；把宏体的两个参数带上（）即可 ((x)*(x))
//	printf("%d\n", i);
//	printf("%d\n", r);
//
//	return 0;
//}
//
//#define ADD(a,b,c) a+b+c		//ADD(a,b,c)为宏名和参数；宏体a+b+c
//int main()
//{
//	int i = ADD(1, 2, 3);
//	printf("%d\n", i);
//
//	return 0;
//}

//3.2.3 #define 替换规则
//在程序中扩展#define定义符号和宏时，需要涉及几个步骤。
//1. 在调用宏时，首先对参数进行检查，看看是否包含任何由#define定义的符号。如果是，它们首先被替换。
//2. 替换文本随后被插入到程序中原来文本的位置。对于宏，参数名被他们的值所替换。
//3. 最后，再次对结果文件进行扫描，看看它是否包含任何由#define定义的符号。如果是，就重复上述处理过程。
//注意：
//1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏，不能出现递归。
//2. 当预处理器搜索#define定义的符号的时候，字符串常量的内容并不被搜索。

//3.2.4 #和##
//#define PRINT(N) printf("the value of "#N" is %d\n",N);
//int main()
//{
//	int a = 10;
//	PRINT1(a);	//printf("the value of ""a"" is %d\n",a);	//#a会把一个宏参数变成对应的字符串"a"；字符串又可以合并
//
//	return 0;
//}

//## 的作用
//##可以把位于它两边的符号合成一个符号。		class##106=class106
//它允许宏定义从分离的文本片段创建标识符

//3.2.5 带副作用的宏参数
//当宏参数在宏的定义中出现超过一次的时候，如果参数带有副作用，那么你在使用这个宏的时候就可能
//出现危险，导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。比如++a
//#define MAX(a,b) ( (a)>(b)?(a):(b) )
//int main()
//{
//	//int m = MAX(2, 3);
//	//printf("%d\n", m);
//
//	int a = 5;
//	int b = 4;
//	int m = MAX(a++, b++);		//(a++)>(b++)?(a++):(b++)==>6>5?7
//
//	printf("%d\n", m);		//6
//	printf("a=%d b=%d", a, b);		//7 5
//	return 0;
//}

//3.2.6 宏和函数对比
//1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
//2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等可以用于 > 来比较的类型。

//宏的缺点：当然和函数相比宏也有劣势的地方：
//1. 每次使用宏的时候，一份宏定义的代码将插入到程序中。除非宏比较短，否则可能大幅度增加程序的长度。
//2. 宏是没法调试的。
//3. 宏由于类型无关，也就不够严谨。
//4. 宏可能会带来运算符优先级的问题，导致程容易出现错。

//宏有时候可以做函数做不到的事情。比如：宏的参数可以出现类型，但是函数做不到。
//#define MALLOC(num, type) (type *)malloc((num) * sizeof(type))
//int main()
//{
//	int* p = MALLOC(10, int);	//预处理器替换之后：(int*)malloc(10 * sizeof(int));
//
//	return 0;
//}


//3.2.7 命名约定
//一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
//那我们平时的一个习惯是：
//把宏名全部大写 ADD
//函数名不要全部大写 Add

/* 3.3 #undef */
#undef NAME
//如果现存的一个名字需要被重新定义，那么它的旧名字首先要被移除。

/* 3.4 命令行定义 */
//许多C 的编译器提供了一种能力，允许在命令行中定义符号。用于启动编译过程。
//例如：当我们根据同一个源文件要编译出一个程序的不同版本的时候，这个特性有点用处。（假定某个
//程序中声明了一个某个长度的数组，如果机器内存有限，我们需要一个很小的数组，但是另外一个机器内存大些，我们需要一个数组能够大些。）
//在linux环境下用gcc演示


/* 3.5 条件编译 */
//在编译一个程序的时候我们如果要将一条语句（一组语句）编译或者放弃是很方便的。因为我们有条件编译指令。
//#define __DEBUG__		//不让代码编译注释即可
//int main()
//{
//	int i = 0;
//	int arr[10] = { 0 };
//	for (i = 0; i < 10; i++)
//	{
//		arr[i] = i;
//#ifdef __DEBUG__
//		printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 
//#endif //__DEBUG__
//	}
//	return 0;
//}

//常见的条件编译指令：
//1.
#if 常量表达式		//常量表达式为真就编译
//...
#endif
//常量表达式由预处理器求值。
如：
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif

//2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

//3.判断是否被定义
//#if defined(symbol)		//如果定义了symbol就参与编译
//#ifdef symbol
//#if !defined(symbol)	//如果没有定义symbol就执参与编译
//#ifndef symbol

//4.嵌套指令
//#if defined(OS_UNIX)
//#ifdef OPTION1
//unix_version_option1();
//#endif
//#ifdef OPTION2
//unix_version_option2();
//#endif
//#elif defined(OS_MSDOS)
//#ifdef OPTION2
//msdos_version_option2();
//#endif
//#endif


/* 3.6 文件包含*/
//我们已经知道， #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
//这种替换的方式很简单：
//预处理器先删除这条指令，并用包含文件的内容替换。
//这样一个源文件被包含10次，那就实际被编译10次。

//可在头文件中加上一段代码：#ifndef __TEST_H__		防止头文件被重复定义；#pragma once也可以实现
//							#define __TEST_H__
//							...
//							#endif
//		



//3.6.1 头文件被包含的方式：